package com.flca.pstc42.util;

import java.util.ArrayList;

import org.garret.perst.Key;
import org.garret.perst.Storage;
import org.garret.perst.StorageFactory;

import com.flca.pstc42.dbs.MyRootClass;
import com.flca.pstc42.dto.AdresDto;
import com.flca.pstc42.dto.CityDto;
import com.flca.pstc42.dto.DistanceDto;
import com.flca.pstc42.dto.PostcodesDto;
import com.flca.pstc42.dto.StreetDto;

public class PstcUtils {

	public static final String ENVDBSDIR_KEY = "pstc42.dbs.name";
	
	public static final int M = 1000000;
	
	private static Storage db;
	private static MyRootClass root;
	
	
	/**
	 * This returns an AdresDto if one can be found or null.
	 * <br>
	 * example: AdresDto adres = PcTools.findAdres(5672, "VB", 67);
	 * @param aPcNum a number between 1000 and 9999
	 * @param aPcAlfa a two character code (not case sensitive)
	 * @param aHousNr a number > 0 and < 10000
	 * @return AdresDto or null
	 */
	public static AdresDto findAdres(int aPcNum, String aPcAlfa, int aHousNr) throws Exception
	{
		validateInput(aPcNum, aPcAlfa, aHousNr);
		return doFindAdres(aPcNum, aPcAlfa, aHousNr);
	}

	/**
	 * This returns an AdresDto if one can be found or null.
	 * <br>
	 * example: AdresDto adres = PcTools.findAdres(5672, "VB");
	 * @param aPcNum a number between 1000 and 9999
	 * @param aPcAlfa a two character code (not case sensitive)
	 * @return AdresDto or null. Note the houseNumber will be empty
	 */
	public static AdresDto findAdres(int aPcNum, String aPcAlfa) throws Exception
	{
		validateInput(aPcNum, aPcAlfa);
		return doFindAdres(aPcNum, aPcAlfa, 0);
	}

	/**
	 * This returns an AdresDto if one can be found or null.
	 * <br>
	 * example: AdresDto adres = PcTools.findAdres(5672);
	 * @param aPcNum a number between 1000 and 9999
	 * @return AdresDto or null. Note the houseNumber and city will be empty
	 */
	public static AdresDto findAdres(int aPcNum) throws Exception
	{
		validateInput(aPcNum);
		return doFindAdres(aPcNum, null, 0);
	}

	/**
	 * This returns an AdresDto if one can be found or null.
	 * <br>
	 * example: AdresDto adres = PcTools.findAdres(5672, "VB", 67);
	 * @param aPcNum a number between 1000 and 9999
	 * @param aPcAlfa a two character code (not case sensitive)
	 * @param aHousNr a number > 0 and < 10000
	 * @return AdresDto or null
	 */
	private static AdresDto doFindAdres(int aPcNum, String aPcAlfa, int aHousNr) throws Exception
	{
		AdresDto result = null;
		
		if (root == null) {
			setup();
		}
		
    	try {
    		String pckey = makeDbEntryKey(aPcNum, aPcAlfa);
            for (PostcodesDto pcDto : root.postcodePk.prefixIterator(pckey)) { 

            	if (foundAdres(pcDto, aHousNr)) {
        			result = makeAdresDto(aPcNum, aPcAlfa, aHousNr, pcDto);
        			break;
        		}
            }
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
		}
		
		return result;
	}

	private static String makeDbEntryKey(int aPcNum, String aPcAlfa) throws Exception
	{
		if (aPcAlfa != null) {
			String pckey = "" + aPcNum + aPcAlfa.toUpperCase();
			return pckey;
		} else {
			String pckey = "" + aPcNum + "AA";
			return pckey;
		}
	}
	
	private static boolean foundAdres(PostcodesDto aPcDto, int aHousNr)
	{
		if (aHousNr == 0) {
			return true;
		} else {
	   		int soort = getSoort(aHousNr);
			
	   		return (aPcDto.getSoort() == soort && 
	   				aPcDto.getVan() <= aHousNr && 
	   				aPcDto.getTem() >= aHousNr); 
		}
	}
	
	private static void validateInput(int aPcNum) throws Exception
	{
		if (aPcNum < 1000 || aPcNum > 9999) {
			throw new Exception("" +  aPcNum + " not valid. It should be a number between 1000 and 9999");
		} else {
			return;
		}
	}
	
	private static void validateInput(int aPcNum, String aPcAlfa) throws Exception
	{
		validateInput(aPcNum);
		
		if (aPcAlfa == null || aPcAlfa.trim().length() != 2) {
			throw new Exception("" +  aPcAlfa + " not valid. It should be a 2 character code");
		} else {
			return;
		}
	}

	private static void validateInput(int aPcNum, String aPcAlfa, int aHousNr) throws Exception
	{
		validateInput(aPcNum, aPcAlfa);
		
		if (aHousNr <= 0 || aHousNr > 9999) {
			throw new Exception("" +  aHousNr + " not valid. It should be a number between 0 and 9999");
		} else {
			return;
		}
	}
	
	/**
	 * This calculates the geometrical distance in km between two locations given by the AdresDto's, and it attemts
	 * to retrieve the travelDistance using a google webservice.
	 * @param adres1
	 * @param adres2
	 * @return
	 */
	public static DistanceDto calcDistance(AdresDto adres1, AdresDto adres2)
	{
		DistanceDto result = new DistanceDto();
		result.setFromAdres(adres1);
		result.setToAdres(adres2);
		result.setDistance(CalcDistanceHelper.calcDistance(adres1.getLat(), adres1.getLng(), adres2.getLat(), adres2.getLng()));
		CalcTravelDistanceHelper.getTravelDistance(result);
		return result;
	}
	
	/**
	 * This calculates the distance in km between two locations given by the geocodes
	 * @param adres1
	 * @param adres2
	 * @return
	 */
	public static double calcDistance(double lat1, double lng1, double lat2, double lng2)
	{
		return CalcDistanceHelper.calcDistance(lat1, lng1, lat2, lng2);
	}
	
	private static AdresDto makeAdresDto(int aPcNum, String aPcAlfa,
			int aHousNr, PostcodesDto pcDto) 
	{
		AdresDto result;
		result = new AdresDto();
		result.setPcNum(aPcNum);
		result.setPcAlfa(aPcAlfa);
		result.setHouseNr(aHousNr);
		result.setLat(1.0*pcDto.getLat() / M);
		result.setLng(1.0*pcDto.getLng() / M);
		
		StreetDto streetdto = root.streetPk.get(new Key(pcDto.getStraatid()));
		result.setStreet(streetdto.getStreet());

		CityDto citydto = root.cityPk.get(new Key(streetdto.getCityId()));	
		result.setCity(citydto.getCity());
		result.setProvince(citydto.getProvince());
		
		return result;
	}


	private static int getSoort(int aHousNr) 
	{
		int even = 0;
		if (aHousNr % 2 == 0) {
			even = 1;
		}
		return even;
	}
    
	static final int POOL_SIZE = 32*1024*1024; // database cache size
	
	/**
	 * This method can be used at the start of the application to open all the postcode tables.
	 * It is not required however.<br>
	 * The parameter should conatain the directory will the Berkeley postcode tables
	 */
	public static synchronized void setup(String aDbsName)
	{
        db = StorageFactory.getInstance().createStorage();
        db.open(aDbsName, POOL_SIZE);

        root = (MyRootClass)db.getRoot(); // get storage root
        if (root == null) { 
            root = new MyRootClass(db); // create root object
            db.setRoot(root); // register root object
        }		
	}
	
	/**
	 * This method be used at the start of the application to open all the postcodes tables.
	 * It is not required however.
	 */
	public static synchronized void setup()
	{
		setup(getDbsName());
	}
	
	public static String getDbsName() 
	{
		String envdir = System.getProperty(ENVDBSDIR_KEY);
		
		if (envdir != null) {
			return envdir;
		} else {
			throw new RuntimeException("the system property " + ENVDBSDIR_KEY + " has not been set");
		}
//		else {
//			URL url = Pstc42Tools.class.getResource(Pstc42Tools.class.getName());
//			System.out.println(url);
//			String path = url.getPath().substring(0, url.getPath().lastIndexOf("/"));
//			// go up 1 dir
//			path = path.substring(0, path.lastIndexOf("/"));
//			
//			path += "/META-INF/postcodes";
//			return new File(path);
//		}
	}
	
	/**
	 * shitdowns the database
	 */
	public static void shutdown() 
	{
		try {
			System.out.println("closing postcode database");
			if (db != null) {
				db.close();
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * This returns the minimum postal code (numeric part) and the max postal code, and the count found 
	 * These values are returned in a int array. result[0] = min, result[1] = max, result[2] = count of found postcodes
	 * inside window
	 * The default rsturn values are 0 - 9999. Only when the lower-left and/or upper-right are within the 
	 * postcode range, then the return values may vary. 
	 * @param aMinLat
	 * @param aMaxLat
	 * @param aMinLng
	 * @param aMaxLng
	 * @return
	 */
	public static int[] getMinMaxPostcodeNum(double aMinLat, double aMaxLat, double aMinLng, double aMaxLng)
	{
		int result[] = new int[] {0, 0, 0};

		int minLat = (int) (aMinLat * M);
		int minLng = (int) (aMinLng * M);
		int maxLat = (int) (aMaxLat * M);
		int maxLng = (int) (aMaxLng * M);

    	try {
    		Key minLatKey = new Key(minLat); 
    		Key maxLatKey = new Key(maxLat); 
    		Key minLngKey = new Key(minLng); 
    		Key maxLngKey = new Key(maxLng); 

    		ArrayList<PostcodesDto> list1 = root.geoLatFk.getList(minLatKey, maxLatKey);
    		ArrayList<PostcodesDto> list2 = root.geoLngFk.getList(minLngKey, maxLngKey);

    		if (list1.size() < list2.size()) {
    			checkPostNum(minLat, minLng, maxLat, maxLng, list1, result);
    		} else {
    			checkPostNum(minLat, minLng, maxLat, maxLng, list2, result);
    		}
    		
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
		}

		return result;
	}

	private static void checkPostNum(int minLat, int minLng,
			int maxLat, int maxLng, ArrayList<PostcodesDto> list2, int result[]) 
	{
		for (PostcodesDto pc2 : list2) {
			if (isInside(pc2, minLat, maxLat, minLng, maxLng)) {
				result[2]++;
				int pcnum = Integer.parseInt(pc2.getPostcode().substring(0, 4));
				if (result[0] == 0 || result[0] > pcnum) {
					result[0] = pcnum;
				}

				if (result[0] == 0 || result[1] < pcnum) {
					result[1] = pcnum;
				}
			}
		}
	}
	
	/**
	 * Checks if the given AdresDto is inside the rectangle indicated by the coordinates
	 * @param pcDto
	 * @param aMinLat
	 * @param aMaxLat
	 * @param aMinLng
	 * @param aMaxLng
	 * @return
	 */
	public static boolean isInside(AdresDto aAdresDto, double aMinLat, double aMaxLat, double aMinLng, double aMaxLng)
	{
		return aAdresDto.getLat() >= aMinLat && aAdresDto.getLat() <= aMaxLat &&
		aAdresDto.getLng() >= aMinLng && aAdresDto.getLng() <= aMaxLng; 
	}
	
	/**
	 * Checks if the given PostcodesDto is inside the rectangle indicated by the coordinates
	 * @param pcDto
	 * @param aMinLat
	 * @param aMaxLat
	 * @param aMinLng
	 * @param aMaxLng
	 * @return
	 */
	private static boolean isInside(PostcodesDto pcDto, int aMinLat, int aMaxLat, int aMinLng, int aMaxLng)
	{
		return pcDto.getLat() >= aMinLat && pcDto.getLat() <= aMaxLat &&
				pcDto.getLng() >= aMinLng && pcDto.getLng() <= aMaxLng; 
	}

	@Override
	protected void finalize() throws Throwable 
	{
		shutdown();
		super.finalize();
	}
	
	
}
